// Copyright 2012 The golab Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package machine

// It should be noted that there is more than one way to achieve synchronous and
// asynchronous behavior when using RPC for remote command execution.  We have
// chosen to background the commands on the remote side and make all synchronous
// RPC calls.

import (
	"fmt"
	"log"
	"net/rpc"
	"os"
	"sync"

	"../cmd"
)

// A Client exists on the control machine as a way of sending RPC calls to the
// individual end-systems (where a Local process is running).
type Client struct {
	Host    string                 // Network ID for the machine (can be an IP address)
	Port    int                    // RPC port to use (default is 50007)
	Results map[string]*cmd.Result `json:"-"` // List of result objects
	ResPath string                 `json:"-"` // Path to use for saving results
	Log     *log.Logger            `json:"-"` // Logger to use for the client level RPC log
	c       *rpc.Client            // RPC client
	mux     sync.Mutex             // Mutex to protect the Results map
	lf      *os.File               // File used as output by the Log
}

// setup prepares a client for RPC calls.  The logging is also setup here so
// the ResPath should be set before calling this.
func (m *Client) setup() error {
	if m.Port == 0 {
		m.Port = stdRPCPort
	}
	if m.ResPath == "" {
		return fmt.Errorf("setup:m.ResPath missing (Host=%s)", m.Host)
	}
	if m.Results == nil {
		m.Results = make(map[string]*cmd.Result)
	}
	err := os.MkdirAll(m.ResPath, os.ModeDir|os.ModePerm)
	if err != nil {
		return err
	}
	m.lf, err = os.Create(fmt.Sprintf("%s/rpc.log", m.ResPath))
	if err != nil {
		e, ok := err.(*os.PathError)
		if ok {
			return e.Err
		}
		return err
	}
	m.Log = log.New(m.lf, "", log.LstdFlags)
	m.c, err = rpc.DialHTTP("tcp", fmt.Sprintf("%s:%d", m.Host, m.Port))
	if err != nil {
		m.Log.Printf("ERROR: setup:rpc.DialHTTP: %v", err)
		return err
	}
	m.Log.Printf("ok: setup: (Host=%s Port=%d)", m.Host, m.Port)
	return err
}

// Run will synchronously run a command on a client.
func (m *Client) Run(c *cmd.Cmd) (*cmd.Result, error) {
	if m.c == nil {
		err := m.setup()
		if err != nil {
			return nil, err
		}
	}
	if c.ID == "" {
		c.SetID()
	}
	r := &cmd.Result{}
	err := m.c.Call("Local.Run", c, r)
	if err != nil {
		m.Log.Printf("ERROR: Run:m.c.Call: %v (cmdID=%s)", err, c.ID)
	} else {
		m.Log.Printf("ok: Run:m.c.Call: (cmdID=%s)", c.ID)
	}
	return r, err

}

// Start will asynchronously start a command on a client.
func (m *Client) Start(c *cmd.Cmd) error {
	if m.c == nil {
		err := m.setup()
		if err != nil {
			return err
		}
	}
	if c.ID == "" {
		c.SetID()
	}
	err := m.c.Call("Local.Start", c, nil)
	if err != nil {
		m.Log.Printf("ERROR: Start:m.c.Call: %v (cmdID=%s)", err, c.ID)
	} else {
		m.Log.Printf("ok: Start:m.c.Call: (cmdID=%s)", c.ID)
	}
	return err
}

// Start will asynchronously start a command on a client replacing any argument
// to the command that is "TARGET" with the given target string.
func (m *Client) StartTarget(c *cmd.Cmd, target string) error {
	if m.c == nil {
		err := m.setup()
		if err != nil {
			return err
		}
	}
	if c.ID == "" {
		c.SetID()
	}
	tc := &cmd.Cmd{
		ID:   c.ID,
		Path: c.Path,
		Args: make([]string, len(c.Args)),
		Dir:  c.Dir,
	}
	for i := range c.Args {
		if c.Args[i] == "TARGET" {
			tc.Args[i] = target
		} else {
			tc.Args[i] = c.Args[i]
		}
	}
	err := m.c.Call("Local.Start", tc, nil)
	if err != nil {
		m.Log.Printf("ERROR: StartTarget:m.c.Call: %v (cmdID=%s target=%s)", err, tc.ID, target)
	} else {
		m.Log.Printf("ok: StartTarget:m.c.Call: (cmdID=%s target=%s)", tc.ID, target)
	}
	return err
}

// Wait will block on a previously started asynchronous command and return the
// results.
func (m *Client) Wait(cmdID string) (*cmd.Result, error) {
	if m.c == nil {
		return nil, ErrCmdMissing
	}
	r := &cmd.Result{}
	err := m.c.Call("Local.Wait", cmdID, r)
	if err == nil {
		m.Log.Printf("ok: Wait:m.c.Call: (cmdID=%s)", cmdID)
		m.mux.Lock()
		defer m.mux.Unlock()
		m.Results[cmdID] = r
	} else {
		m.Log.Printf("ERROR: Wait:m.c.Call: %v (cmdID=%s)", err, cmdID)
	}
	return r, err
}

// Wait will block on a previously started asynchronous command and return the
// results after sending the SIGINT signal to the child process.
func (m *Client) Stop(cmdID string) (*cmd.Result, error) {
	if m.c == nil {
		return nil, ErrCmdMissing
	}
	r := &cmd.Result{}
	err := m.c.Call("Local.Stop", cmdID, r)
	if err == nil {
		m.Log.Printf("ok: Stop:m.c.Call: (cmdID=%s)", cmdID)
		m.mux.Lock()
		defer m.mux.Unlock()
		m.Results[cmdID] = r
	} else {
		m.Log.Printf("ERROR: Stop:m.c.Call: %v (cmdID=%s)", err, cmdID)
	}
	return r, err
}

// Wait will block on a previously started asynchronous command and return the
// results after sending the SIGKILL signal to the child process.
func (m *Client) Kill(cmdID string) (*cmd.Result, error) {
	if m.c == nil {
		return nil, ErrCmdMissing
	}
	r := &cmd.Result{}
	err := m.c.Call("Local.Kill", cmdID, r)
	if err == nil {
		m.Log.Printf("ok: Kill:m.c.Call: (cmdID=%s)", cmdID)
		m.mux.Lock()
		defer m.mux.Unlock()
		m.Results[cmdID] = r
	} else {
		m.Log.Printf("ERROR: Kill:m.c.Call: %v (cmdID=%s)", err, cmdID)
	}
	return r, err
}

// Save writes the results of a given command to a set of files.  The stepID is
// used as a prefix to avoid overwriting in the case where commands are used
// more than once by the same machine.
func (m *Client) Save(cmdID, stepID string) error {
	r := m.Results[cmdID]
	if r == nil {
		return ErrCmdMissing
	}
	err := os.MkdirAll(m.ResPath, os.ModeDir|os.ModePerm)
	if err != nil {
		m.Log.Printf("ERROR: Save:os.MkdirAll: %v (path=%s)", err, m.ResPath)
		return err
	}
	err = r.Save(m.ResPath, stepID)
	if err != nil {
		m.Log.Printf("ERROR: Save:r.Save: %v (path=%s)", err, m.ResPath)
	} else {
		m.Log.Printf("ok: Save:r.Save: (cmdID=%s stepID=%s)", cmdID, stepID)
	}
	return err
}

// SaveAll saves all commands that were run on the client to their respective files.
func (m *Client) SaveAll() error {
	err := os.MkdirAll(m.ResPath, os.ModeDir|os.ModePerm)
	if err != nil {
		m.Log.Printf("ERROR: SaveAll:os.MkdirAll: %v (path=%s)", err, m.ResPath)
		return err
	}
	i := -1
	for _, v := range m.Results {
		i++
		if v == nil {
			return ErrCmdMissing
		}
		err = v.Save(m.ResPath, fmt.Sprintf("%d", i))
		if err != nil {
			return err
		}
	}
	return nil
}

// Close will shut down a client gracefully closing open files in the process.
func (m *Client) Close() error {
	err := m.c.Close()
	if err != nil {
		m.Log.Printf("ERROR: Close:m.c.Close: %v", err)
	} else {
		m.Log.Print("ok: Close:m.c.Close")
	}
	m.Log.Print("CLOSING LOG FILE")
	m.lf.Close()
	return err
}

// A Local is an RPC server.  It is the counter part of the Client.
//
// Most of the time there will be no need to interact with these directly.
// Ideally they sit dumbly on the end-systems waiting for commands to execute.
type Local struct {
	pMap map[string]*cmd.Process // a map of active processes
	mux  sync.Mutex              // a Mutex for protecting the active process map
}

// NewLocal returns a Local that has been setup and ready to use.
func NewLocal() *Local {
	return &Local{
		pMap: make(map[string]*cmd.Process),
	}
}

// Run creates a synchronous subprocess and waits for the results.
func (m *Local) Run(c *cmd.Cmd, r *cmd.Result) error {
	if c.ID == "" {
		c.SetID()
	}
	var err error
	err = c.Run(r)
	return err
}

// Run creates a asynchronous subprocess.
func (m *Local) Start(c *cmd.Cmd, notUsed *string) error {
	if c.ID == "" {
		c.SetID()
	}
	p, err := c.Start()
	if err != nil {
		return err
	}
	m.mux.Lock()
	defer m.mux.Unlock()
	m.pMap[c.ID] = p
	return err
}

// Wait blocks on a previously started asynchronous subprocess returning the
// results.
func (m *Local) Wait(cmdID string, r *cmd.Result) error {
	m.mux.Lock()
	p := m.pMap[cmdID]
	delete(m.pMap, cmdID)
	m.mux.Unlock()
	if p == nil {
		return ErrCmdMissing
	}
	var err error
	err = p.Wait(r)
	return err
}

// Wait blocks on a previously started asynchronous subprocess returning the
// results after sending the SIGINT signal.
func (m *Local) Stop(cmdID string, r *cmd.Result) error {
	m.mux.Lock()
	p := m.pMap[cmdID]
	delete(m.pMap, cmdID)
	m.mux.Unlock()
	if p == nil {
		return ErrCmdMissing
	}
	var err error
	err = p.Stop(r)
	return err
}

// Wait blocks on a previously started asynchronous subprocess returning the
// results after sending the SIGKILL signal.
func (m *Local) Kill(cmdID string, r *cmd.Result) error {
	m.mux.Lock()
	p := m.pMap[cmdID]
	delete(m.pMap, cmdID)
	m.mux.Unlock()
	if p == nil {
		return ErrCmdMissing
	}
	var err error
	err = p.Kill(r)
	return err
}
